﻿// Copyright 2010 Maxime Serrano <lastchronicler@gmail.com>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Text;

namespace MCSharp {
    public sealed class Player : Entity {
        public static Player Console;
        public Session session;
        public string name;
        public string nick;
        public string DisconnectReason = null;
        public PlayerInfo info;
        public short currentItem;
        internal DateTime idleTimer = DateTime.UtcNow;
        public const int ChunkArea = 8;  // Size along X and Z of loaded chunks at any point
        public int LoadedChunksMinX = 0;
        public int LoadedChunksMinZ = 0;
        public Position homePos;
        public bool HasChunks = false;
        public static Block[] nonPlaceable = new Block[] { Block.Admincrete, Block.Water, Block.StillWater,
                                                            Block.Lava, Block.StillLava, Block.TNT, Block.Undefined};
        public List<Player> localPlayers = new List<Player>();
        object chunkUpdateLock = new object();
        public bool InstantBreak = false;

        #region Constructors

        // Constructor for other players (Console, IRC, etc.)
        public Player(string playerName) {
            name = playerName;
            nick = name;
            info = new PlayerInfo(playerName, ClassList.highestClass);
        }

        // Constructor for just-joined player
        public Player(string playerName, Session playerSession, Position playerSpawn) {
            name = playerName;
            nick = name;
            session = playerSession;
            info = PlayerDB.FindPlayerInfo(this);
            pos = playerSpawn;
        }

        #endregion

        #region Chunk Updates

        // Check if chunks need to be loaded / unloaded
        public void UpdateChunks() {
            lock (chunkUpdateLock) {
                // Get neccesary chunk range
                int RequiredMinX = (int)((int)pos.x / 16) - (ChunkArea / 2);
                int RequiredMinZ = (int)((int)pos.z / 16) - (ChunkArea / 2);

                // Check if new chunks are required
                if (RequiredMinX == LoadedChunksMinX && RequiredMinZ == LoadedChunksMinZ)
                    return;

                // Send Pre-Chunks and Chunks
                for (int Xchunk = RequiredMinX; Xchunk < (RequiredMinX + ChunkArea); Xchunk++) {
                    for (int Zchunk = RequiredMinZ; Zchunk < (RequiredMinZ + ChunkArea); Zchunk++) {
                        if ((Xchunk >= (LoadedChunksMinX + ChunkArea) || Xchunk < (LoadedChunksMinX) || Zchunk >= (LoadedChunksMinZ + ChunkArea) || Zchunk < (LoadedChunksMinZ)) || (!HasChunks)) {
                            Chunk chunkToSend = Server.world.GetChunk(Xchunk, Zchunk);
                            if (chunkToSend == null) {
                                Logger.Log("Player.UpdateChunks: Error loading or generating chunk! ({0}, {1})", LogType.Error, Xchunk, Zchunk);
                                session.KickNow("ERROR: Failed to load or generate chunk! (" + Xchunk + ", " + Zchunk + ")");
                                return;
                            }
                            chunkToSend.AddPlayer(this);
                            if (!chunkToSend.terrainPopulated)
                                chunkToSend.CalculateLighting();

                            session.SendNow(PacketWriter.MakePreChunk(Xchunk, Zchunk, true));
                            session.SendNow(PacketWriter.MakeChunk(chunkToSend));
                        }
                    }
                }

                // Unload old chunks (if they have old chunks)
                if (HasChunks) {
                    for (int Xchunk = LoadedChunksMinX; Xchunk < (LoadedChunksMinX + ChunkArea); Xchunk++) {
                        for (int Zchunk = LoadedChunksMinZ; Zchunk < (LoadedChunksMinZ + ChunkArea); Zchunk++) {
                            if ((Xchunk >= (RequiredMinX + ChunkArea) || Xchunk < (RequiredMinX) || Zchunk >= (RequiredMinZ + ChunkArea) || Zchunk < (RequiredMinZ)) && (HasChunks)) {
                                session.SendNow(PacketWriter.MakePreChunk(Xchunk, Zchunk, false));
                                Chunk chunkToRemove = Server.world.GetChunk(Xchunk, Zchunk);
                                if (chunkToRemove != null)
                                    chunkToRemove.RemovePlayer(this);
                            }
                        }
                    }
                } else {
                    HasChunks = true;
                    session.SendNow(PacketWriter.MakePositionAndLook(pos));
                }

                // Set new loaded chunk range
                LoadedChunksMinX = RequiredMinX;
                LoadedChunksMinZ = RequiredMinZ;
            }
        }

        #endregion

        #region Blocks

        public bool DestroyBlock(int x, byte y, int z) {
            // Check for special types
            if (Server.world.GetBlock(x, y, z).type == (byte)Block.Admincrete)
                return false;
            if (Server.world.GetBlock(x, y, z).type == (byte)Block.Air)
                return false;

            // Set block
            Server.world.SetBlock(new ChunkBlock(Block.Air, 0, 0x00), x, y, z, true);

            // Update block
            Server.SendToAllInRange(PacketWriter.MakeBlockChange(Server.world.GetBlock(x, y, z), x, y, z), this);

            return true;
        }

        public bool PlaceBlock(byte type, int x, byte y, int z, byte dir) {
            // START TEMP
            switch (dir) {
                case 0:
                    y--;
                    break;
                case 1:
                    y++;
                    break;
                case 2:
                    z--;
                    break;
                case 3:
                    z++;
                    break;
                case 4:
                    x--;
                    break;
                case 5:
                    x++;
                    break;
            }
            Server.world.SetBlock(new ChunkBlock(Block.Dirt, 0, 0), x, y, z, true);
            Server.SendToAllInRange(PacketWriter.MakeBlockChange(new ChunkBlock(Block.Dirt, 0, 0), x, y, z), this);
            return false;
            // END TEMP

            /* if (!CanPlace((Block)type)) {
                Message(Color.Red + "Hey! You can't place that!");
                return false;
            }

            // If item, stop
            if (type >= 255)
                return false;

            // If invalid, stop
            if (type < 0)
                return false;

            // Find block coordinate
            switch (dir) {
                case 0:
                    y--;
                    break;
                case 1:
                    y++;
                    break;
                case 2:
                    z--;
                    break;
                case 3:
                    z++;
                    break;
                case 4:
                    x--;
                    break;
                case 5:
                    x++;
                    break;
            }

            // Get existing block, and stop if not water, air, or lava
            ChunkBlock existingBlock = Server.world.GetBlock(x, y, z);
            if(existingBlock.type != (byte)Block.Air &&
                existingBlock.type != (byte)Block.Water &&
                existingBlock.type != (byte)Block.Lava &&
                existingBlock.type != (byte)Block.StillWater &&
                existingBlock.type != (byte)Block.StillLava)
                return false;

            // Make block
            bool allowPlace = true;
            byte placedLight = 0;
            if (Blocks.LightCarryingBlocks.Contains((Block)type))
                placedLight = existingBlock.lighting;
            foreach (BlockInfo info in Blocks.LightEmittingBlocks) {
                if (info.type == (Block)type) {
                    placedLight = info.info;
                    break;
                }
            }
            ChunkBlock placedBlock = new ChunkBlock((byte)type, 0x00, placedLight);

            // Block logic
            switch ((Block)type) {
                case Block.Torch:
                    if (dir == 0)
                        allowPlace = false;
                    else if (dir == 1)
                        placedBlock.metadata = 0x00;
                    else if (dir == 2)
                        placedBlock.metadata = 0x04;
                    else if (dir == 3)
                        placedBlock.metadata = 0x03;
                    else if (dir == 4)
                        placedBlock.metadata = 0x02;
                    else if (dir == 5)
                        placedBlock.metadata = 0x01;
                    placedBlock.lighting = placedLight;
                    break;
            }

            // If not allowed to place, send real block to player and stop
            if (!allowPlace) {
                Send(PacketWriter.MakeBlockChange(existingBlock, x, y, z));
                return false;
            }

            // Set and update block
            Server.world.SetBlock(placedBlock, x, y, z, true);
            Server.SendToAllInRange(PacketWriter.MakeBlockChange(placedBlock, x, y, z), this);

            return true; */
        }

        #endregion

        #region Template Messages

        internal void NoPlayerMessage(string name) {
            Message("No players found matching \"" + name + "\"");
        }


        internal void ManyPlayersMessage(string name) {
            Message("More than one player found matching \"" + name + "\"");
        }


        internal void NoAccessMessage(params Permission[] permissions) {
            Message(Color.Red + "You do not have access to this command.");
            if (permissions.Length == 1) {
                Message(Color.Red + "You need " + permissions[0].ToString() + " permission.");
            } else {
                Message(Color.Red + "You need the following permissions:");
                foreach (Permission permission in permissions) {
                    Message(Color.Red + permission.ToString());
                }
            }
        }

        #endregion

        #region Chat / Messages

        // Queues a system message
        public void Message(string message) {
            Message(">", message);
        }

        // Queues a wrapped system message with a custom color
        public void Message(string prefix, string message) {
            if (session == null) {
                Logger.LogConsole(message);
            } else {
                foreach (Packet p in PacketWriter.MakeWrappedMessage(prefix, Color.Sys + message, false)) {
                    session.Send(p);
                }
            }
        }

        // Parses message incoming from the player
        public void ParseMessage(string message, bool fromConsole) {
            switch (CommandList.GetMessageType(message)) {
                case MessageType.Chat:
                    if (!Can(Permission.Chat)) return;
                    //if (CheckChatSpam()) return;
                    info.linesWritten++;
                    string displayedName = nick;
                    if (Config.GetBool(ConfigKey.ClassColorsInChat) && info.playerClass.color.Length > 0 && info.playerClass.color != Color.White) {
                        displayedName = info.playerClass.color + displayedName + Color.White;
                    }

                    if (name == "dag10") displayedName = "§4d§ca§eg§a1§b0§f";
                    if (name == "fragmer") displayedName = "§4f§cr§ea§ag§bm§9e§5r§f";
                    Server.SendToAll("<" + displayedName + "> " + message);
                    Logger.Log("{0}: {1}", LogType.WorldChat, GetLogName(), message);
                    break;

                case MessageType.Command:
                    Logger.Log("{0}: {1}", LogType.UserCommand, GetLogName(), message);
                    CommandList.ParseCommand(this, message, fromConsole);
                    break;

                case MessageType.PrivateChat:
                    if (!Can(Permission.Chat)) return;
                    //if (CheckChatSpam()) return;
                    string otherPlayerName = message.Substring(1, message.IndexOf(' ') - 1);
                    Player otherPlayer = Server.FindPlayer(otherPlayerName);
                    if (otherPlayer != null) {
                        Logger.Log("{0} to {1}: {2}", LogType.PrivateChat, GetLogName(), otherPlayer.GetLogName(), message);
                        otherPlayer.Message(Color.Gray + "from " + name + ": " + message.Substring(message.IndexOf(' ') + 1));
                        Message(Color.Gray + "to " + otherPlayer.name + ": " + message.Substring(message.IndexOf(' ') + 1));
                    } else {
                        NoPlayerMessage(otherPlayerName);
                    }
                    break;

                case MessageType.ClassChat:
                    if (!Can(Permission.Chat)) return;
                    string className = message.Substring(2, message.IndexOf(' ') - 2);
                    PlayerClass playerClass = ClassList.FindClass(className);
                    if (playerClass != null) {
                        Logger.Log("{0} to class {1}: {2}", LogType.ClassChat, GetLogName(), playerClass.name, message);
                        Packet classMsg = PacketWriter.MakeMessage(Color.Gray + "[" + playerClass.color + playerClass.name + Color.Gray + "]" + nick + ": " + message.Substring(message.IndexOf(' ') + 1));
                        Server.SendToClass(classMsg, playerClass);
                        if (info.playerClass != playerClass) {
                            Send(classMsg);
                        }
                    } else {
                        Message("No class found matching \"" + className + "\"");
                    }
                    break;
            }
        }

        // safety wrapper for session.Send
        public void Send(Packet packet) {
            if (session != null) session.Send(packet);
        }

        public void Send(Packet packet, bool isHighPriority) {
            if (session != null) session.Send(packet, isHighPriority);
        }

        #endregion

        #region Permission Checks

        public bool Can(params Permission[] permissions) {
            foreach (Permission permission in permissions) {
                return info.playerClass.Can(permission);
            }
            return true;
        }

        public bool CanDraw(int volume) {
            return (info.playerClass.drawLimit > 0) && (volume > info.playerClass.drawLimit);
        }

        #endregion

        #region Utils

        // Get log name
        public string GetLogName() {
            return name;
        }

        // Validates player name and chat messages
        public static bool CheckForIllegalChars(string message) {
            // are we sure that this is a proper implementation?
            for (int i = 0; i < message.Length; i++) {
                char ch = message[i];
                if (ch < ' ' || ch == '§' || ch == '`' || ch > '}') {
                    return true;
                }
            }
            return false;
        }

        // ensures that player name has the correct length and character set
        public static bool IsValidName(string name) {
            if (name.Length < 2 || name.Length > 16) return false;
            for (int i = 0; i < name.Length; i++) {
                char ch = name[i];
                if (ch < '0' || (ch > '9' && ch < 'A') || (ch > 'Z' && ch < '_') || (ch > '_' && ch < 'a') || ch > 'z') {
                    return false;
                }
            }
            return true;
        }

        internal void ResetIdleTimer() {
            idleTimer = DateTime.UtcNow;
        }

        // Determines if a player can place a certain block
        public bool CanPlace(Block block) {
            if (Can(Permission.PlaceAnything))
                return true;
            return(!(nonPlaceable.Contains(block)));
        }

        #endregion
    }
}
